﻿// =====================================================================
//
//  This file is part of the Microsoft Dynamics CRM SDK code samples.
//
//  Copyright (C) Microsoft Corporation.  All rights reserved.
//
//  This source code is intended only as a supplement to Microsoft
//  Development Tools and/or on-line documentation.  See these other
//  materials for detailed information regarding Microsoft code samples.
//
//  THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
//  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//  PARTICULAR PURPOSE.
//
// =====================================================================

using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Threading;
using System.Xml.Linq;

namespace SL.IdentityModel.Protocols.WSTrust
{
	/// <summary>
	/// class is used to make ws-trust RequestSecurityToken requests using trust13, and only the issue action
	/// </summary>
	public class WSTrustClient : ClientBase<IWSTrustContract>, IWSTrustContract
	{


		private RequestSecurityTokenResponse _rstr;

		/// <summary>
		/// Class is used by the WSTrustClient to make WSTrust Issue requests
		/// </summary>
		private class WSTrustClientChannel : ChannelBase<IWSTrustContract>, IWSTrustContract
		{
			public WSTrustClientChannel(System.ServiceModel.ClientBase<IWSTrustContract> client) :
				base(client)
			{ }

			public System.IAsyncResult BeginIssue(Message message, System.AsyncCallback callback, object asyncState)
			{
				object[] _args = new object[1];
				_args[0] = message;
				System.IAsyncResult _result = base.BeginInvoke("Issue", _args, callback, asyncState);
				return _result;
			}

			public Message EndIssue(System.IAsyncResult result)
			{
				object[] _args = new object[0];
				Message resultmsg = null;

				resultmsg = ((Message)(base.EndInvoke("Issue", _args, result)));

				return resultmsg;
			}
		}

		protected string IssueAction = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue";

		private BeginOperationDelegate _onBeginIssueDelegate;
		private EndOperationDelegate _onEndIssueDelegate;
		private SendOrPostCallback _onIssueCompletedDelegate;

		public event System.EventHandler<IssueCompletedEventArgs> IssueCompleted;


		/// <summary>
		/// Creates an instance of the class.
		/// 
		/// Bindings dervived from WSTrustBinding create bindings compatable with specified credential types
		/// 
		/// WSTrustBindingUserNamaMixed  -> UsernameCredentials
		/// WSTrustBindignWindows -> WindowsCredentials
		/// 
		/// </summary>
		/// <param name="binding">binding to be use for contacting the security token service</param>
		/// <param name="remoteAddress">endoint of securitytoken service</param>
		/// <param name="credentials">credential used to authentiate to the security token service</param>
		public WSTrustClient(Binding binding, EndpointAddress remoteAddress, IRequestCredentials credentials)
			: base(binding, remoteAddress)
		{
			if (null == credentials)
			{
				throw new ArgumentNullException("credentials");
			}

			if (credentials is UsernameCredentials)
			{
				UsernameCredentials usernname = credentials as UsernameCredentials;
				base.ChannelFactory.Credentials.UserName.UserName = usernname.Username;
				base.ChannelFactory.Credentials.UserName.Password = usernname.Password;
			}
			else if (credentials is IssuedTokenCredentials)
			{
				var issuedToken = credentials as IssuedTokenCredentials;
				_rstr = issuedToken.RSTR;
			}
			else if (credentials is WindowsCredentials)
			{ }
			else
			{
				throw new ArgumentOutOfRangeException("credentials", "type was not expected");
			}
		}

		/// <summary>
		/// Starts async service call to request security token
		/// once complete, eventhandler IssueCompleted is called
		/// </summary>
		/// <param name="rst">Provides RequestSecurityToken to use in the request</param>
		public void IssueAsync(RequestSecurityToken rst)
		{
			this.IssueAsync(rst, null);
		}

		/// <summary>
		/// Starts async service call to request security token
		/// once complete, eventhandler IssueCompleted is called
		/// </summary>
		/// <param name="rst">Provides RequestSecurityToken to use in the request</param>
		/// <param name="userState">userstate to be passe to async call</param>
		public void IssueAsync(RequestSecurityToken rst, object userState)
		{
			if (null == _onBeginIssueDelegate)
			{
				_onBeginIssueDelegate = new BeginOperationDelegate(this.OnBeginIssue);
			}
			if (null == _onEndIssueDelegate)
			{
				_onEndIssueDelegate = new EndOperationDelegate(this.OnEndIssue);
			}
			if (null == _onIssueCompletedDelegate)
			{
				_onIssueCompletedDelegate = new SendOrPostCallback(this.OnIssueCompleted);
			}
			object[] newUserState = new object[2];
			newUserState[0] = rst;
			newUserState[1] = userState;

			base.InvokeAsync(_onBeginIssueDelegate, new object[] {
                        rst }, _onEndIssueDelegate, _onIssueCompletedDelegate, newUserState);
		}


		/// <summary>
		/// Creates WSTrust channel
		/// </summary>
		/// <returns></returns>
		protected override IWSTrustContract CreateChannel()
		{
			return new WSTrustClientChannel(this);
		}

		/// <summary>
		/// Given a RequestSecurityToken object, creates the message for the SOAP call
		/// </summary>
		/// <param name="request">RequestSecurityToken</param>
		/// <returns>Message</returns>
		private Message BuildRequestAsMessage(RequestSecurityToken request)
		{
			var message = Message.CreateMessage(base.Endpoint.Binding.MessageVersion ?? MessageVersion.Default, IssueAction, (BodyWriter)new WSTrustRequestBodyWriter(request, this.IssueAction));
			if (_rstr != null)
			{
				message.Headers.Add(new IssuedTokenHeader(_rstr));
			}
			return message;
		}

		private IAsyncResult OnBeginIssue(object[] inValues, System.AsyncCallback callback, object asyncState)
		{
			RequestSecurityToken rst = inValues[0] as RequestSecurityToken;

			Message request = BuildRequestAsMessage(rst);
			return this.Channel.BeginIssue(request, callback, asyncState);
		}

		private object[] OnEndIssue(System.IAsyncResult result)
		{
			Message retVal = ((WSTrustClient)(this)).EndIssue(result);

			WSTrustResponseSerializer rstrSerializer = BuildResponseSerializer(this.IssueAction);
			RequestSecurityTokenResponse rstr = null;

			try
			{
				rstr = rstrSerializer.ReadXml(retVal as Message);
			}
			catch (ArgumentNullException)
			{
				throw new FaultException(MessageFault.CreateFault(retVal, 2000), "http://www.w3.org/2005/08/addressing/soap/fault");
			}

			return new object[] { rstr };
		}

		private WSTrustResponseSerializer BuildResponseSerializer(XNamespace issueAction)
		{
			if (issueAction == "http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue")
				return new WSTrust13ResponseSerializer();
			else
				return new WSTrust2005ResponseSerializer();
		}

		/// <summary>
		/// Is called once async operation is complete
		/// calls eventhandler IssueCompleted
		/// </summary>
		/// <param name="state">userstate</param>
		private void OnIssueCompleted(object state)
		{
			if (null != IssueCompleted)
			{
				InvokeAsyncCompletedEventArgs e = ((InvokeAsyncCompletedEventArgs)(state));
				object[] results = e.Results as object[];
				if (null != results && results[0] is RequestSecurityTokenResponse)
				{
					RequestSecurityTokenResponse rstr = results[0] as RequestSecurityTokenResponse;
					rstr.RST = ((object[])e.UserState)[0] as RequestSecurityToken;
				}
				IssueCompleted(this, new IssueCompletedEventArgs(e.Results, e.Error, e.Cancelled, ((object[])e.UserState)[1]));
			}
		}

		#region IWSTrustClient

		/// <summary>
		/// begins the async call for token issuance
		/// </summary>
		/// <param name="message">XML of message to be used in teh call</param>
		/// <param name="callback">Method to call once async call is complete</param>
		/// <param name="asyncState">userstate passed into call</param>
		/// <returns></returns>
		public IAsyncResult BeginIssue(Message message, AsyncCallback callback, object asyncState)
		{
			return asyncState as IAsyncResult;
		}

		/// <summary>
		/// Called on completion of async call
		/// </summary>
		/// <param name="asyncResult"></param>
		/// <returns></returns>
		public Message EndIssue(IAsyncResult asyncResult)
		{
			return base.Channel.EndIssue(asyncResult);
		}

		#endregion

	}
}
